// --------------------------------------------------------------------------------------------------------------------- 
// <copyright file="GlobalKeyboardHook.cs" company="TechBit">
//   TechBit 2009.
// </copyright>
// <author>
//   Leandro G. Vacirca
// </author>
// <date>
//   22/03/2009
// </date>
// <summary>
//   Defines the GlobalKeyboardHook type.
// </summary>
// ---------------------------------------------------------------------------------------------------------------------
namespace IntelliShortCut.Core
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;

    /// <summary>
    /// A class that manages a global low level keyboard hook.
    /// </summary>
    public class GlobalKeyboardHook
    {
        #region "Constants"

        /// <summary>
        /// Installs a hook procedure that monitors low-level keyboard input events.
        /// </summary>
        private const int WH_KEYBOARD_LL = 13;

        /// <summary>
        /// The WM_KEYDOWN message is posted to the window with the keyboard focus when a nonsystem key is pressed. 
        /// A nonsystem key is a key that is pressed when the ALT key is not pressed.
        /// </summary>
        private const int WM_KEYDOWN = 0x100;

        /// <summary>
        /// The WM_KEYUP message is posted to the window with the keyboard focus when a nonsystem key is released. 
        /// A nonsystem key is a key that is pressed when the ALT key is not pressed, or a keyboard key that is pressed when a window has the keyboard focus.
        /// </summary>
        private const int WM_KEYUP = 0x101;

        /// <summary>
        /// The WM_SYSKEYDOWN message is posted to the window with the keyboard focus when the user presses the F10 key (which activates the menu bar) or holds down the ALT key and then presses another key. 
        /// It also occurs when no window currently has the keyboard focus; in this case, the WM_SYSKEYDOWN message is sent to the active window.
        /// </summary>
        private const int WM_SYSKEYDOWN = 0x104;

        /// <summary>
        /// The WM_SYSKEYUP message is posted to the window with the keyboard focus when the user releases a key that was pressed while the ALT key was held down. 
        /// It also occurs when no window currently has the keyboard focus; in this case, the WM_SYSKEYUP message is sent to the active window.
        /// </summary>
        private const int WM_SYSKEYUP = 0x105;

        #endregion

        #region "Private Members"

        /// <summary>
        /// The collections of keys to watch for.
        /// </summary>
        private readonly List<Keys> _hookedKeys = new List<Keys>();

        /// <summary>
        /// Handle to the hook, need this to unhook and call the next hook.
        /// </summary>
        private IntPtr _hHook = IntPtr.Zero;

        #endregion

        #region "Constructors and Destructors"

        /// <summary>
        /// Initializes a new instance of the <see cref="GlobalKeyboardHook"/> class and installs the keyboard hook.
        /// </summary>
        public GlobalKeyboardHook() 
        {
            Hook();
        }

        /// <summary>
        /// Finalizes an instance of the <see cref="GlobalKeyboardHook"/> class. 
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="GlobalKeyboardHook"/> is reclaimed by garbage collection and uninstalls the keyboard hook.
        /// </summary>
        ~GlobalKeyboardHook() 
        {
            UnHook();
        }

        #endregion

        #region "Delegates"

        /// <summary>
        /// The LowLevelKeyboardProc hook procedure is an application-defined or library-defined callback function used with the SetWindowsHookEx function. 
        /// The system calls this function every time a new keyboard input event is about to be posted into a thread input queue.
        /// </summary>
        /// <param name="code">
        /// Specifies a code the hook procedure uses to determine how to process the message. 
        /// If nCode is less than zero, the hook procedure must pass the message to the CallNextHookEx function without further processing and should return the value returned by CallNextHookEx. 
        /// This parameter can be one of the following values.
        /// HC_ACTION: The wParam and lParam parameters contain information about a keyboard message.
        /// </param>
        /// <param name="wParam"> 
        /// Specifies the identifier of the keyboard message. 
        /// This parameter can be one of the following messages: WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, or WM_SYSKEYUP.
        /// </param>
        /// <param name="lParam">
        /// Pointer to a <see cref="KeyboardHookStruct"/> structure.
        /// </param>
        /// <returns>
        /// If nCode is less than zero, the hook procedure must return the value returned by <see cref="CallNextHookEx"/>.
        /// If nCode is greater than or equal to zero, and the hook procedure did not process the message, it is highly recommended that you call CallNextHookEx 
        /// and return the value it returns; otherwise, other applications that have installed WH_KEYBOARD_LL hooks will not receive hook notifications and may behave 
        /// incorrectly as a result. If the hook procedure processed the message, it may return a nonzero value to prevent the system from passing the message to the rest 
        /// of the hook chain or the target window procedure.
        /// </returns>
        public delegate int LowLevelKeyboardHookProc(int code, int wParam, ref KeyboardHookStruct lParam);

        #endregion

        #region "Events"

        /// <summary>
        /// Occurs when one of the hooked keys is pressed
        /// </summary>
        public event KeyEventHandler KeyDown;

        /// <summary>
        /// Occurs when one of the hooked keys is released
        /// </summary>
        public event KeyEventHandler KeyUp;

        #endregion

        #region "Public Methods"

        /// <summary>
        /// Adds a key that will be watched.
        /// </summary>
        /// <param name="key">The key to watch.</param>
        public void AddKey(Keys key)
        {
            if (_hookedKeys.Contains(key))
            {
                return;
            }

            _hookedKeys.Add(key);
        }

        /// <summary>
        /// Removes a key.
        /// </summary>
        /// <param name="key">The key to remove.</param>
        public void RemoveKey(Keys key)
        {
            if (!_hookedKeys.Contains(key))
            {
                return;
            }

            _hookedKeys.Remove(key);
        }

        #endregion

        #region "DLL Imports"

        /// <summary>
        /// The SetWindowsHookEx function installs an application-defined hook procedure into a hook chain.
        /// </summary>
        /// <param name="idHook">
        /// Specifies the type of hook procedure to be installed (eg WH_KEYBOARD).
        /// </param>
        /// <param name="lpfn">
        /// Pointer to the hook procedure. If the dwThreadId parameter is zero or specifies the identifier of a thread created by a different process, the lpfn parameter must point to a hook procedure in a DLL. 
        /// Otherwise, lpfn can point to a hook procedure in the code associated with the current process.</param>
        /// <param name="hMod">
        /// Handle to the DLL containing the hook procedure pointed to by the lpfn parameter. 
        /// The hMod parameter must be set to NULL if the dwThreadId parameter specifies a thread created by the current process and if the hook procedure is within the code associated with the current process.</param>
        /// <param name="threadId">
        /// Specifies the identifier of the thread with which the hook procedure is to be associated. 
        /// If this parameter is zero, the hook procedure is associated with all existing threads running in the same desktop as the calling thread.</param>
        /// <returns>
        /// If the function succeeds, the return value is the handle to the hook procedure.
        /// If the function fails, the return value is NULL.
        /// </returns>
        [DllImport("user32.dll")]
        private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardHookProc lpfn, IntPtr hMod, uint threadId);

        /// <summary>
        /// The UnhookWindowsHookEx function removes a hook procedure installed in a hook chain by the <see cref="SetWindowsHookEx"/> function.
        /// </summary>
        /// <param name="hInstance">
        /// Handle to the hook to be removed. This parameter is a hook handle obtained by a previous call to <see cref="SetWindowsHookEx"/>.
        /// </param>
        /// <returns>
        /// The hook procedure can be in the state of being called by another thread even after UnhookWindowsHookEx returns. 
        /// If the hook procedure is not being called concurrently, the hook procedure is removed immediately before UnhookWindowsHookEx returns.
        /// </returns>
        [DllImport("user32.dll")]
        private static extern bool UnhookWindowsHookEx(IntPtr hInstance);

        /// <summary>
        /// The CallNextHookEx function passes the hook information to the next hook procedure in the current hook chain. 
        /// A hook procedure can call this function either before or after processing the hook information.
        /// </summary>
        /// <param name="idHook">
        /// Handle to the current hook. 
        /// An application receives this handle as a result of a previous call to the SetWindowsHookEx function.
        /// </param>
        /// <param name="nCode">
        /// Specifies the hook code passed to the current hook procedure. 
        /// The next hook procedure uses this code to determine how to process the hook information.
        /// </param>
        /// <param name="wParam">
        /// Specifies the wParam value passed to the current hook procedure. 
        /// The meaning of this parameter depends on the type of hook associated with the current hook chain.
        /// </param>
        /// <param name="lParam">
        /// Specifies the lParam value passed to the current hook procedure. 
        /// The meaning of this parameter depends on the type of hook associated with the current hook chain.
        /// </param>
        /// <returns>
        /// Hook procedures are installed in chains for particular hook types. 
        /// CallNextHookEx calls the next hook in the chain.
        /// </returns>
        [DllImport("user32.dll")]
        private static extern int CallNextHookEx(IntPtr idHook, int nCode, int wParam, ref KeyboardHookStruct lParam);

        /// <summary>
        /// Loads the specified module into the address space of the calling process. 
        /// The specified module may cause other modules to be loaded.
        /// </summary>
        /// <param name="lpFileName">
        /// The name of the module. This can be either a library module (a .dll file) or an executable module (an .exe file). 
        /// The name specified is the file name of the module and is not related to the name stored in the library module itself, as specified by the LIBRARY keyword in the module-definition (.def) file.
        /// </param>
        /// <returns>
        /// If the function succeeds, the return value is a handle to the module. 
        /// If the function fails, the return value is NULL.
        /// </returns>
        [DllImport("kernel32.dll")]
        private static extern IntPtr LoadLibrary(string lpFileName);

        #endregion

        #region "Private Methods"

        /// <summary>
        /// Installs the global keyboard hook.
        /// </summary>
        private void Hook()
        {
            var instance = LoadLibrary("User32");
            _hHook = SetWindowsHookEx(WH_KEYBOARD_LL, HookProc, instance, 0);
        }

        /// <summary>
        /// Uninstalls the global keyboard hook.
        /// </summary>
        private void UnHook()
        {
            UnhookWindowsHookEx(_hHook);
        }

        /// <summary>
        /// The LowLevelKeyboardProc hook procedure is an application-defined or library-defined callback function used with the SetWindowsHookEx function. 
        /// The system calls this function every time a new keyboard input event is about to be posted into a thread input queue.
        /// </summary>
        /// <param name="code">
        /// Specifies a code the hook procedure uses to determine how to process the message. 
        /// If nCode is less than zero, the hook procedure must pass the message to the CallNextHookEx function without further processing and should return the value returned by CallNextHookEx. 
        /// This parameter can be one of the following values.
        /// HC_ACTION: The wParam and lParam parameters contain information about a keyboard message.
        /// </param>
        /// <param name="wParam">
        /// Specifies the identifier of the keyboard message. 
        /// This parameter can be one of the following messages: WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, or WM_SYSKEYUP.
        /// </param>
        /// <param name="lParam">
        /// Pointer to a <see cref="KeyboardHookStruct"/> structure.
        /// </param>
        /// <returns>
        /// If nCode is less than zero, the hook procedure must return the value returned by <see cref="CallNextHookEx"/>.
        /// If nCode is greater than or equal to zero, and the hook procedure did not process the message, it is highly recommended that you call CallNextHookEx 
        /// and return the value it returns; otherwise, other applications that have installed WH_KEYBOARD_LL hooks will not receive hook notifications and may behave 
        /// incorrectly as a result. If the hook procedure processed the message, it may return a nonzero value to prevent the system from passing the message to the rest 
        /// of the hook chain or the target window procedure.
        /// </returns>
        private int HookProc(int code, int wParam, ref KeyboardHookStruct lParam)
        {
            if (code >= 0)
            {
                var key = (Keys)lParam.VirtualKeyCode;
                if (_hookedKeys.Contains(key))
                {
                    var kea = new KeyEventArgs(key);
                    if ((wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) && (KeyDown != null))
                    {
                        KeyDown(this, kea);
                    }
                    else if ((wParam == WM_KEYUP || wParam == WM_SYSKEYUP) && (KeyUp != null))
                    {
                        KeyUp(this, kea);
                    }

                    if (kea.Handled)
                    {
                        return 1;
                    }
                }
            }

            return CallNextHookEx(_hHook, code, wParam, ref lParam);
        }

        #endregion

        #region "Structures"

        /// <summary>
        /// The KeyboardHookStruct structure contains information about a low-level keyboard input event.
        /// </summary>
        public struct KeyboardHookStruct
        {
            /// <summary>
            /// Specifies a virtual-key code. The code must be a value in the range 1 to 254.
            /// </summary>
            public int VirtualKeyCode;

            /// <summary>
            /// Specifies a hardware scan code for the key.
            /// </summary>
            public int ScanCode;

            /// <summary>
            /// Specifies the extended-key flag, event-injected flag, context code, and transition-state flag.
            /// </summary>
            public int Flags;

            /// <summary>
            /// Specifies the time stamp for this message, equivalent to what GetMessageTime would return for this message.
            /// </summary>
            public int Time;

            /// <summary>
            /// Specifies extra information associated with the message.
            /// </summary>
            public int ExtraInfo;
        }

        #endregion
    }
}